/*****************************************************************************
*
*  PROJECT:     Multi Theft Auto v1.2
*  LICENSE:     See LICENSE in the top level directory
*  FILE:        FileSystem/src/fsinternal/CFileSystem.zip.internal.h
*  PURPOSE:     Master header of ZIP archive filesystem private modules
*
*  Multi Theft Auto is available from http://www.multitheftauto.com/
*
*****************************************************************************/

#ifndef _FILESYSTEM_ZIP_PRIVATE_MODULES_
#define _FILESYSTEM_ZIP_PRIVATE_MODULES_

// ZIP extension struct.
struct zipExtension
{
    static fileSystemFactory_t::pluginOffset_t _zipPluginOffset;

    static zipExtension*        Get( CFileSystem *sys )
    {
        if ( _zipPluginOffset != fileSystemFactory_t::INVALID_PLUGIN_OFFSET )
        {
            return fileSystemFactory_t::RESOLVE_STRUCT <zipExtension> ( (CFileSystemNative*)sys, _zipPluginOffset );
        }
        return NULL;
    }

    void                        Initialize      ( CFileSystemNative *sys );
    void                        Shutdown        ( CFileSystemNative *sys );

    inline void operator = ( const zipExtension& right )
    {
        assert( 0 );
    }

    CArchiveTranslator*         NewArchive      ( CFile& writeStream );
    CArchiveTranslator*         OpenArchive     ( CFile& readWriteStream );

    // Extension members.
    CFileSystemNative*          fileSys;
};

// Checksums.
#define ZIP_SIGNATURE               0x06054B50
#define ZIP_FILE_SIGNATURE          0x02014B50
#define ZIP_LOCAL_FILE_SIGNATURE    0x04034B50

#include <time.h>

#include "CFileSystem.vfs.h"
#include "CFileSystem.FileDataPresence.h"

#pragma warning(push)
#pragma warning(disable:4250)

class CZIPArchiveTranslator : public CSystemPathTranslator, public CFileTranslatorWideWrap, public CArchiveTranslator
{
    friend struct zipExtension;
    friend class CArchiveFile;
public:
                    CZIPArchiveTranslator( CFileSystemNative *fileSys, zipExtension& theExtension, CFile& file );
                    ~CZIPArchiveTranslator( void );

    bool            CreateDir( const char *path ) override;
    CFile*          Open( const char *path, const char *mode, eFileOpenFlags flags ) override;
    bool            Exists( const char *path ) const override;
    bool            Delete( const char *path ) override;
    bool            Copy( const char *src, const char *dst ) override;
    bool            Rename( const char *src, const char *dst ) override;
    size_t          Size( const char *path ) const override;
    bool            Stat( const char *path, struct stat *stats ) const override;

protected:
    bool            OnConfirmDirectoryChange( const dirTree& tree ) override;

public:
    void            ScanDirectory( const char *directory, const char *wildcard, bool recurse,
                        pathCallback_t dirCallback,
                        pathCallback_t fileCallback,
                        void *userdata ) const override;

    void            GetDirectories( const char *path, const char *wildcard, bool recurse, std::vector <filePath>& output ) const override;
    void            GetFiles( const char *path, const char *wildcard, bool recurse, std::vector <filePath>& output ) const override;

    void            Save( void ) override;

    // Members.
    zipExtension&   m_zipExtension;
    CFile&          m_file;

#pragma pack(1)
    struct _localHeader
    {
        endian::little_endian <fsUInt_t>        signature;
        endian::little_endian <fsUShort_t>      version;
        endian::little_endian <fsUShort_t>      flags;
        endian::little_endian <fsUShort_t>      compression;
        endian::little_endian <fsUShort_t>      modTime;
        endian::little_endian <fsUShort_t>      modDate;
        endian::little_endian <fsUInt_t>        crc32val;

        endian::little_endian <fsUInt_t>        sizeCompressed;
        endian::little_endian <fsUInt_t>        sizeReal;

        endian::little_endian <fsUShort_t>      nameLen;
        endian::little_endian <fsUShort_t>      commentLen;
    };

    struct _centralFile
    {
        endian::little_endian <fsUInt_t>        signature;
        endian::little_endian <fsUShort_t>      version;
        endian::little_endian <fsUShort_t>      reqVersion;
        endian::little_endian <fsUShort_t>      flags;
        endian::little_endian <fsUShort_t>      compression;
        endian::little_endian <fsUShort_t>      modTime;
        endian::little_endian <fsUShort_t>      modDate;
        endian::little_endian <fsUInt_t>        crc32val;

        endian::little_endian <fsUInt_t>        sizeCompressed;
        endian::little_endian <fsUInt_t>        sizeReal;

        endian::little_endian <fsUShort_t>      nameLen;
        endian::little_endian <fsUShort_t>      extraLen;
        endian::little_endian <fsUShort_t>      commentLen;

        endian::little_endian <fsUShort_t>      diskID;
        endian::little_endian <fsUShort_t>      internalAttr;
        endian::little_endian <fsUInt_t>        externalAttr;
        endian::little_endian <fsUInt_t>        localHeaderOffset;
    };
#pragma pack()

private:
    void            ReadFiles( unsigned int count );    // throws an exception on failure.

public:
    struct fileMetaData;
    struct directoryMetaData;

public:
    struct genericMetaData abstract
    {
        VirtualFileSystem::fsObjectInterface *genericInterface;

        inline void SetInterface( VirtualFileSystem::fsObjectInterface *intf )
        {
            this->genericInterface = intf;
        }

        unsigned short  version;
        unsigned short  reqVersion;
        unsigned short  flags;

        unsigned short  diskID;

        unsigned short  modTime;
        unsigned short  modDate;

        size_t          localHeaderOffset;

        std::string     extra;
        std::string     comment;

        inline _localHeader ConstructLocalHeader( void ) const
        {
            _localHeader header;
            header.signature = ZIP_LOCAL_FILE_SIGNATURE;
            header.modTime = modTime;
            header.modDate = modDate;
            header.nameLen = (fsUShort_t)this->genericInterface->GetRelativePath().size();
            header.commentLen = (fsUShort_t)comment.size();
            return header;
        }

        inline _centralFile ConstructCentralHeader( void ) const
        {
            _centralFile header;
            header.signature = ZIP_FILE_SIGNATURE;
            header.version = version;
            header.reqVersion = reqVersion;
            header.flags = flags;
            //header.compression
            header.modTime = modTime;
            header.modDate = modDate;
            //header.crc32
            //header.sizeCompressed
            //header.sizeReal
            header.nameLen = (fsUShort_t)this->genericInterface->GetRelativePath().size();
            header.extraLen = (fsUShort_t)extra.size();
            header.commentLen = (fsUShort_t)comment.size();
            header.diskID = diskID;
            //header.internalAttr
            //header.externalAttr
            header.localHeaderOffset = (fsUInt_t)localHeaderOffset;
            return header;
        }

        inline void AllocateArchiveSpace( CZIPArchiveTranslator *archive, _localHeader& entryHeader, size_t& sizeCount )
        {
            // Update the offset.
            localHeaderOffset = archive->m_file.Tell() - archive->m_structOffset;

            sizeCount += sizeof( entryHeader ) + entryHeader.nameLen + entryHeader.commentLen;
        }

        inline void Reset( void )
        {
#ifdef _WIN32
            version = 10;
#else
            version = 0x03; // Unix
#endif //_WIN32
            reqVersion = 0x14;
            flags = 0;

            UpdateTime();

            diskID = 0;

            extra.clear();
            comment.clear();
        }

        inline void SetModTime( const tm& date )
        {
            unsigned short year = date.tm_year;

            if ( date.tm_year > 1980 )
            {
                year -= 1980;
            }
            else if ( date.tm_year > 80 )
            {
                year -= 80;
            }

            modDate = date.tm_mday | ((date.tm_mon + 1) << 5) | (year << 9);
            modTime = date.tm_sec >> 1 | (date.tm_min << 5) | (date.tm_hour << 11);
        }

        inline void GetModTime( tm& date ) const
        {
            date.tm_mday = modDate & 0x1F;
            date.tm_mon = ((modDate & 0x1E0) >> 5) - 1;
            date.tm_year = ((modDate & 0x0FE00) >> 9) + 1980;

            date.tm_hour = (modTime & 0xF800) >> 11;
            date.tm_min = (modTime & 0x7E0) >> 5;
            date.tm_sec = (modTime & 0x1F) << 1;

            date.tm_wday = 0;
            date.tm_yday = 0;
        }

#pragma warning(push)
#pragma warning(disable: 4996)

        virtual void UpdateTime( void )
        {
            time_t curTime = time( NULL );
            SetModTime( *gmtime( &curTime ) );
        }

#pragma warning(pop)
    };

    class stream;

    enum class eFileDataState
    {
        ARCHIVED,
        PRESENT,
        PRESENT_COMPRESSED
    };

    struct fileMetaData : public genericMetaData
    {
        inline fileMetaData( void ) : dataRefCount( 0 )
        {
            this->dataStream = NULL;
            this->hasDataLocalMods = false;
        }

        inline ~fileMetaData( void )
        {
            assert( this->dataRefCount == 0 );

            if ( CFile *dataStream = this->dataStream )
            {
                delete dataStream;

                this->dataStream = NULL;
            }
        }

        unsigned short  compression;

        unsigned int    crc32val;
        size_t          sizeCompressed;
        size_t          sizeReal;

        unsigned short  internalAttr;
        unsigned int    externalAttr;

        eFileDataState  dataState;

        typedef std::list <stream*> lockList_t;
        lockList_t locks;

        bool            hasDataLocalMods;

    private:
        CFile*          dataStream;

        std::atomic <unsigned long> dataRefCount;

    public:
        CZIPArchiveTranslator *translator;

        inline void Reset( void )
        {
            // Reset generic data.
            genericMetaData::Reset();

            // File-specific reset.
            compression = 8;

            crc32val = 0;
            sizeCompressed = 0;
            sizeReal = 0;
            internalAttr = 0;
            externalAttr = 0;

            dataState = eFileDataState::ARCHIVED;

            assert( this->dataRefCount == 0 );      // nobody must be using our data.

            hasDataLocalMods = false;

            if ( CFile *dataStream = this->dataStream )
            {
                delete dataStream;
            }

            dataStream = NULL;
        }

        // API for obtaining and updating the dataStream.
        inline void SetDataStream( CFile *dataStream )
        {
            // TODO: of course this needs a lock :-)

            CFile *curDataStream = this->dataStream;

            if ( curDataStream == dataStream )
                return;

            assert( this->dataRefCount == 0 );

            if ( curDataStream )
            {
                delete curDataStream;
            }

            this->dataStream = dataStream;
        }

        inline CFile* AcquireDataStream( void )
        {
            CFile *curDataStream = this->dataStream;

            if ( !curDataStream )
                return NULL;

            this->dataRefCount++;

            return curDataStream;
        }

        inline void GarbageCollectDataStream( void )
        {
            if ( this->hasDataLocalMods || this->dataRefCount > 0 )
                return;

            // Release the data stream.
            if ( CFile *dataStream = this->dataStream )
            {
                delete dataStream;
            }

            this->dataStream = NULL;
            this->dataState = eFileDataState::ARCHIVED;
            this->hasDataLocalMods = false;
        }

        inline void ReleaseDataStream( void )
        {
            assert( this->dataRefCount >= 1 );

            this->dataRefCount--;

            this->GarbageCollectDataStream();
        }

        inline bool IsLocked( void ) const
        {
            return ( locks.empty() == false );
        }

        inline bool IsNative( void ) const
        {
#ifdef _WIN32
            return version == 10;
#else
            return version == 0x03; // Unix
#endif //_WIN32
        }

        inline void UpdateTime( void )
        {
            genericMetaData::UpdateTime();

            // Update the time of its parent directories.
            VirtualFileSystem::fileInterface *ourIntf = dynamic_cast <VirtualFileSystem::fileInterface*> ( this->genericInterface );

            if ( VirtualFileSystem::directoryInterface *parentDirAbs = ourIntf->GetDirectory() )
            {
                directory *parentDir = (directory*)parentDirAbs;

                parentDir->metaData.UpdateTime();
            }
        }

        // Virtual node methods.
        inline fsOffsetNumber_t GetSize( void ) const
        {
            fsOffsetNumber_t resourceSize = 0;

            eFileDataState dataState = this->dataState;

            if ( dataState == eFileDataState::ARCHIVED )
            {
                resourceSize = this->sizeReal;
            }
            else if ( dataState == eFileDataState::PRESENT || dataState == eFileDataState::PRESENT_COMPRESSED )
            {
                if ( CFile *dataStream = this->dataStream )
                {
                    resourceSize = dataStream->GetSizeNative();
                }
            }

            return resourceSize;
        }

        bool OnFileCopy( const dirTree& tree, const filePath& newName ) const;
        bool OnFileRename( const dirTree& tree, const filePath& newName );
        void OnFileDelete( void );

        inline void GetANSITimes( time_t& mtime, time_t& ctime, time_t& atime ) const
        {
            tm date;
            GetModTime( date );

            date.tm_year -= 1900;

            mtime = ctime = atime = mktime( &date );
        }

        inline void GetDeviceIdentifier( dev_t& deviceIdx ) const
        {
            deviceIdx = (dev_t)this->diskID;
        }

        bool CopyAttributesTo( fileMetaData& dstEntry );
    };

private:
    inline void seekFile( const fileMetaData& info, _localHeader& header );

public:
    struct directoryMetaData : public genericMetaData
    {
        VirtualFileSystem::directoryInterface *dirInterface;

        inline void SetInterface( VirtualFileSystem::directoryInterface *intf )
        {
            dirInterface = intf;

            genericMetaData::SetInterface( intf );
        }
    
        inline bool     NeedsWriting( void ) const
        {
            return ( this->dirInterface->IsEmpty() || comment.size() != 0 || extra.size() != 0 );
        }

        inline void     UpdateTime( void )
        {
            genericMetaData::UpdateTime();

            // Update the time of its parent directories.
            if ( VirtualFileSystem::directoryInterface *parent = dirInterface->GetParent() )
            {
                ((directory*)parent)->metaData.UpdateTime();
            }
        }
    };

    // Runtime management for files.
    CFileDataPresenceManager fileMan;

    // Tree of all filesystem objects.
    typedef CVirtualFileSystem <CZIPArchiveTranslator, directoryMetaData, fileMetaData> vfs_t;

    vfs_t m_virtualFS;

    typedef vfs_t::fsActiveEntry fsActiveEntry;
    typedef vfs_t::file file;
    typedef vfs_t::directory directory;
    typedef vfs_t::fileList fileList;

    // Initializators for meta-data.
    inline void InitializeFileMeta( fileMetaData& meta )
    {
        meta.translator = this;

        // Make it default properly.
        meta.Reset();
    }

    inline void ShutdownFileMeta( fileMetaData& meta )
    {
        meta.translator = NULL;
    }

    inline void InitializeDirectoryMeta( directoryMetaData& meta )
    {

    }

    inline void ShutdownDirectoryMeta( directoryMetaData& meta )
    {

    }

    // Special functions for the VFS.
    CFile* OpenNativeFileStream( file *fsObject, eFileMode openMode, unsigned int access );

    void OnRemoveRemoteFile( const dirTree& tree, const filePath& name );

    // Implement the stream now.
    class stream abstract : public CFile
    {
        friend class CZIPArchiveTranslator;
    public:
        stream( CZIPArchiveTranslator& zip, file& info, CFile& sysFile, bool destroyOnClose ) : m_sysFile( sysFile ), m_archive( zip ), m_info( info )
        {
            this->m_destroyFileOnClose = destroyOnClose;

            this->m_ourSeek = 0;

            info.metaData.locks.push_back( this );
        }

        ~stream( void )
        {
            m_info.metaData.locks.remove( this );

            if ( m_destroyFileOnClose )
            {
                delete &m_sysFile;
            }
            else
            {
                m_info.metaData.ReleaseDataStream();
            }
        }

        const filePath& GetPath( void ) const override
        {
            return m_path;
        }

    private:
        inline void PutSeek( void )
        {
            m_sysFile.SeekNative( this->m_ourSeek, SEEK_SET );
        }

        inline void UpdateSeek( void )
        {
            this->m_ourSeek = m_sysFile.TellNative();
        }

        CFile&                      m_sysFile;
        CZIPArchiveTranslator&      m_archive;
        file&                       m_info;
        filePath                    m_path;
        bool                        m_destroyFileOnClose;

        std::atomic <fsOffsetNumber_t>  m_ourSeek;
    };

    // We need to cache data on the disk
    void            Extract( CFile& dstFile, vfs_t::file& info );

    // Stream that decompresses things using deflate.
    class fileDeflate : public stream
    {
        friend class CZIPArchiveTranslator;
    public:
                            fileDeflate( CZIPArchiveTranslator& zip, file& info, CFile& sysFile, bool destroyOnClose );
                            ~fileDeflate( void );

        size_t              Read( void *buffer, size_t sElement, size_t iNumElements ) override;
        size_t              Write( const void *buffer, size_t sElement, size_t iNumElements ) override;
        int                 Seek( long iOffset, int iType ) override;
        int                 SeekNative( fsOffsetNumber_t iOffset, int iType ) override;
        long                Tell( void ) const noexcept override;
        fsOffsetNumber_t    TellNative( void ) const noexcept override;
        bool                IsEOF( void ) const noexcept override;
        bool                Stat( struct stat *stats ) const override;
        void                PushStat( const struct stat *stats ) override;
        void                SetSeekEnd( void ) override;
        size_t              GetSize( void ) const noexcept override;
        fsOffsetNumber_t    GetSizeNative( void ) const noexcept override;
        void                Flush( void ) override;
        bool                IsReadable( void ) const noexcept override;
        bool                IsWriteable( void ) const noexcept override;

    private:
        void                Focus( void );

        bool                m_writeable;
        bool                m_readable;
    };

private:
    void            CacheDirectory( const directory& dir );
    void            SaveDirectory( directory& dir, size_t& size );
    unsigned int    BuildCentralFileHeaders( const directory& dir, size_t& size );

    std::string m_comment;

    size_t      m_structOffset;
};

#pragma warning(pop)

#endif //_FILESYSTEM_ZIP_PRIVATE_MODULES_